/*******************************************************************************
 * Copyright 2011 Towee.net
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/

package net.towee.server.persistence.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import net.towee.model.ProfileArea;
import net.towee.model.ProfileContainer;
import net.towee.model.account.AccountData;
import net.towee.model.account.AccountGroup;
import net.towee.model.account.AccountIdentity;
import net.towee.model.account.AccountProfile;
import net.towee.model.account.Board;
import net.towee.model.account.Connection;
import net.towee.model.account.ContactsContainer;
import net.towee.model.account.ContactsGroup;
import net.towee.model.account.Gadget;
import net.towee.model.account.LoginzaAccount;
import net.towee.model.account.Role;
import net.towee.model.account.RuleItem;
import net.towee.model.account.impl.AccountDataImpl;
import net.towee.model.account.impl.AccountGroupImpl;
import net.towee.model.account.impl.AccountIdentityImpl;
import net.towee.model.account.impl.AccountProfileImpl;
import net.towee.model.account.impl.ContactsGroupImpl;
import net.towee.model.account.impl.RoleImpl;
import net.towee.model.bittorrent.TorrentData;
import net.towee.model.bittorrent.impl.TorrentDataImpl;
import net.towee.model.discussion.Discussion;
import net.towee.model.discussion.DiscussionSubject;
import net.towee.model.discussion.Message;
import net.towee.model.discussion.impl.DiscussionImpl;
import net.towee.model.group.GroupData;
import net.towee.model.group.GroupEnterRequest;
import net.towee.model.group.GroupIdentity;
import net.towee.model.group.GroupProfile;
import net.towee.model.group.RoleContainer;
import net.towee.model.group.impl.GroupDataImpl;
import net.towee.model.group.impl.GroupIdentityImpl;
import net.towee.model.group.impl.GroupProfileImpl;
import net.towee.model.impl.ProfileAreaImpl;
import net.towee.model.repo.AttachmentIdentity;
import net.towee.model.repo.AttachmentInfo;
import net.towee.model.repo.DocumentData;
import net.towee.model.repo.DocumentDiff;
import net.towee.model.repo.DocumentIdentity;
import net.towee.model.repo.DocumentNode;
import net.towee.model.repo.DocumentPatch;
import net.towee.model.repo.DocumentType;
import net.towee.model.repo.NodeData;
import net.towee.model.repo.TreeNode;
import net.towee.model.repo.impl.AttachmentIdentityImpl;
import net.towee.model.repo.impl.AttachmentInfoImpl;
import net.towee.model.repo.impl.DocumentDataImpl;
import net.towee.model.repo.impl.DocumentIdentityImpl;
import net.towee.model.repo.impl.DocumentPatchImpl;
import net.towee.model.repo.impl.NodeDataImpl;
import net.towee.model.utils.Pair;
import net.towee.server.authentication.ConfirmEntry;
import net.towee.server.authentication.PasswordDigest;
import net.towee.server.exceptions.FileSizeLimitException;
import net.towee.server.persistence.AccountStore;
import net.towee.server.persistence.AttachmentStore;
import net.towee.server.persistence.ConfirmStore;
import net.towee.server.persistence.DiscussionStore;
import net.towee.server.persistence.DocumentStore;
import net.towee.server.persistence.GroupStore;
import net.towee.server.persistence.TorrentStore;
import net.towee.shared.exceptions.NameExistsException;
import net.towee.shared.exceptions.PersistenceException;

import org.apache.commons.fileupload.util.LimitedInputStream;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.code.morphia.Datastore;
import com.google.code.morphia.Key;
import com.google.code.morphia.Morphia;
import com.google.code.morphia.query.CriteriaContainer;
import com.google.code.morphia.query.MorphiaKeyIterator;
import com.google.code.morphia.query.Query;
import com.google.code.morphia.query.UpdateOperations;
import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.DBRef;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.gridfs.GridFS;
import com.mongodb.gridfs.GridFSDBFile;
import com.mongodb.gridfs.GridFSInputFile;
import com.mycila.event.internal.guava.base.Preconditions;
import com.mycila.event.internal.guava.base.Strings;

/**
 * Implementation of all store interfaces
 * 
 * @author c58
 */
@SuppressWarnings({ "unchecked", "rawtypes" })
public final class MongoDbStore implements AccountStore, GroupStore, ConfirmStore, AttachmentStore,
		DocumentStore, DiscussionStore, TorrentStore {

	private static final Logger LOG = LoggerFactory.getLogger(MongoDbStore.class);

	public static final String ACCOUNT_COLLECTION = "account";
	public static final String ACCOUNT_PROFILES_COLLECTION = "accountProfiles";
	public static final String ACCOUNT_ROLES_COLLECTION = "accountRoles";
	public static final String ACCOUNT_IDENTITY_COLLECTION = "accountIdentity";
	public static final String ACCOUNT_CONTACTS_COLLECTION = "accountContacts";

	public static final String GROUP_COLLECTION = "groups";
	public static final String GROUP_IDENTITY_COLLECTION = "groupsIdentity";
	public static final String GROUP_PROFILE_COLLECTION = "groupsProfile";

	public static final String DOCUMENTS_COLLECTION = "documents";
	public static final String DOCUMENTS_PATCH_COLLECTION = "documentsPatch";
	public static final String DOCUMENTS_IDENTITY_COLLECTION = "documentsIdentity";

	public static final String NODES_COLLECTION = "nodes";

	public static final String ATTACH_IDENTITY_COLLECTION = "attachmentIdentity";
	public static final String ATTACH_COLLECTION = "attachment";
	public static final String ATTACH_INFO_COLLECTION = "attachmentInfo";

	public static final String DISCUSSION_COLLECTION = "discussion";
	public static final String TORRENTS_COLLECTION = "torrents";

	public static final String EMAIL_CONFIRM_COLLECTION = "emailConfirm";

	private final Datastore ds;
	private final Morphia morphia;

	private GridFS attachmentGrid;
	private long maxFileSize = 123000000;

	public MongoDbStore(Mongo mongo, String name) {
		// Create morphia
		morphia = new Morphia();

		// Map morphia
		mapMorphia();

		// Set datastore
		ds = morphia.createDatastore(mongo, name);
		ds.ensureIndexes();
		ds.ensureCaps();
	}

	private void mapMorphia() {
		morphia.map(AccountDataImpl.class).map(ContactsGroupImpl.class).map(AccountProfileImpl.class)
				.map(ConfirmEntry.class).map(AccountIdentityImpl.class).map(ProfileAreaImpl.class)
				.map(AccountGroupImpl.class).map(NodeDataImpl.class).map(AttachmentIdentityImpl.class)
				.map(AttachmentInfoImpl.class).map(DocumentDataImpl.class).map(TorrentDataImpl.class)
				.map(DocumentPatchImpl.class).map(DiscussionImpl.class);
	}

	@Override
	public void initializeAccountStore() throws PersistenceException {

	}

	/*
	 * AccountStore
	 */
	@Override
	public AccountData createAccount(AccountData account) {
		// Set nickname as null. User must set nickname while
		// applying his information
		account.getAccountIdentity().setNickName(null);

		// If account with given email already exists -> merge new and old
		// by adding an LoginzaAccount to old one
		if (account.getEmails().size() > 0 && account.getAttachedAccounts().getAccountsList().size() > 0)
			try {
				// Get old account
				AccountData oldAcc = getAccountByEmail(account.getEmails().get(0));

				// Attach loginza account
				attachLoginzaAccount(oldAcc.getId(), account.getAttachedAccounts().getAccount(0));

				// Return old account
				return oldAcc;
			} catch (PersistenceException e) {
			}

		// Save references
		ds.save(account.getAccountIdentity());
		ds.save(account.getProfile());

		// Save account
		ds.save(account);
		return account;
	}

	@Override
	public void removeAccount(String id) throws PersistenceException {
		// Get account
		AccountData acc = getAccountById(id, "contacts");

		// Delete references
		ds.delete(AccountIdentityImpl.class, id);
		ds.delete(AccountProfileImpl.class, id);
		ds.delete(acc.getContacts().getContactsGroupsList());

		// Delete account
		ds.delete(AccountDataImpl.class, id);
	}

	@Override
	public AccountData getAccountById(String id) throws PersistenceException {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(id));
		AccountData account = null;

		if ((account = ds.get(AccountDataImpl.class, id)) == null)
			throw new PersistenceException();

		return account;
	}

	@Override
	public AccountData getAccountById(String id, String... fields) throws PersistenceException {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(id));
		AccountData account = ds.createQuery(AccountDataImpl.class).field("_id").equal(id)
				.retrievedFields(true, fields).get();

		if (account == null)
			throw new PersistenceException();

		return account;
	}

	@Override
	public AccountIdentity getAccountIdentity(String string) throws PersistenceException {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(string));
		AccountIdentity acid = ds.get(AccountIdentityImpl.class, string);

		if (acid == null)
			throw new PersistenceException();

		return acid;
	}
	
	@Override
	public String getAccountByCookie(String coockieRememberId) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(coockieRememberId), "null cookie id");
		
		Key<AccountDataImpl> key = ds.find(AccountDataImpl.class).field( "cookieId" ).equal(coockieRememberId).getKey();
		
		if(key != null) 
			return key.getId().toString();
		
		return null;
	}
	
	@Override
	public void updateAccountCookie(String userId, String coockieRememberId) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(coockieRememberId), "null cookie id");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(userId), "null user id");
		
		// Update cookie ID
		ds.update(ds.createQuery(AccountDataImpl.class).field( "_id" ).equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).set("cookieId", coockieRememberId));		
	}

	@Override
	public AccountData getAccountByNickname(String nickName, String... fields)
			throws PersistenceException {
		Query<AccountIdentityImpl> q = ds.createQuery(AccountIdentityImpl.class).field("nickName")
				.equal(nickName);
		AccountIdentity id = q.get();
		if (id != null) {
			Query<AccountDataImpl> qa = ds.createQuery(AccountDataImpl.class).field("_id")
					.equal(id.getId());

			if (fields != null)
				qa.retrievedFields(true, fields);

			AccountData acc = qa.get();

			if (acc == null)
				throw new PersistenceException();

			return acc;
		}

		throw new PersistenceException();
	}

	@Override
	public AccountData getAccountByLoginza(LoginzaAccount logacc) throws PersistenceException {
		AccountData account = null;

		if ((account = ds.createQuery(AccountDataImpl.class).disableValidation().field("loginzaAccounts")
				.equal(logacc).retrievedFields(true, "_id").get()) == null)
			throw new PersistenceException();

		return account;
	}

	@Override
	public AccountData getAccountByEmail(String email, String... fields) throws PersistenceException {
		Preconditions.checkArgument(isNotEmpty(email));
		AccountData account = null;

		Query<AccountDataImpl> q = ds.createQuery(AccountDataImpl.class).field("emails").contains(email);

		if (fields != null)
			q.retrievedFields(true, fields);
		else
			q.retrievedFields(true, "emails", "_id", "salt", "digest");

		if ((account = q.get()) == null)
			throw new PersistenceException();

		return account;
	}

	@Override
	public boolean attachLoginzaAccount(String userId, LoginzaAccount logacc) {
		try {
			// If account exists merge they
			AccountData oldAcc = getAccountByLoginza(logacc);

			// If old acc is base acc
			if (oldAcc.getId().equals(userId))
				return false;

			// Get base acc
			AccountData baseAcc = getAccountById(userId);

			// Merge data
			baseAcc.getAttachedAccounts().getAccountsList()
					.addAll(oldAcc.getAttachedAccounts().getAccountsList());
			baseAcc.getBoardsContainer().getBoardsList()
					.addAll(oldAcc.getBoardsContainer().getBoardsList());
			baseAcc.getContacts().getAllContacts().addAll(oldAcc.getContacts().getAllContacts());
			baseAcc.getEmails().addAll(oldAcc.getEmails());
			baseAcc.getGroups().getGroupsList().addAll(oldAcc.getGroups().getGroupsList());
			((AccountIdentityImpl) baseAcc.getAccountIdentity()).groups
					.addAll(((AccountIdentityImpl) oldAcc.getAccountIdentity()).groups);

			// Update base account. Given attached account was in oldAcc,
			// therefore we don't need to attach it manually.
			ds.save(baseAcc);

			// Remove old acc
			removeAccount(oldAcc.getId());

			return true;
		} catch (PersistenceException e) {
			return ds.update(
					ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
					ds.createUpdateOperations(AccountDataImpl.class)
							.add("loginzaAccounts", logacc, false)).getUpdatedExisting();
		}
	}

	@Override
	public boolean detachLoginzaAccount(String id, LoginzaAccount logacc) {
		return ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(id),
				ds.createUpdateOperations(AccountDataImpl.class).removeAll("loginzaAccounts", logacc))
				.getUpdatedExisting();
	}

	@Override
	public boolean attachEmail(String userId, String email) {
		return ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).add("emails", email, false))
				.getUpdatedExisting();
	}

	@Override
	public boolean detachEmail(String userId, String email) {
		return ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).removeAll("emails", email))
				.getUpdatedExisting();
	}

	@Override
	public boolean setOnlineState(String userId, boolean state) {
		return ds.update(ds.createQuery(AccountIdentityImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountIdentityImpl.class).set("online", state))
				.getUpdatedExisting();
	}

	@Override
	public boolean updateLastSessionDate(String userId) {
		return ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).set("lastSession", new Date()))
				.getUpdatedExisting();
	}

	@Override
	public boolean updateAccountIdentity(String userId, AccountIdentity identity)
			throws PersistenceException {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(userId), "null user id");

		// Get old account
		AccountIdentity oldId = getAccountIdentity(userId);

		// Create update query
		UpdateOperations<AccountIdentityImpl> uq = ds.createUpdateOperations(AccountIdentityImpl.class);
		boolean update = false;

		LOG.info("Start updating nickname: " + identity.toString()+" OLDAcc: "+oldId.toString());

		// If given identity have nickname and old identity doesn't have
		if (!Strings.isNullOrEmpty(identity.getNickName()) && oldId.getNickName() == null) {

			// Check for account existing with given nickname
			if (checkAccountWithNickName(identity.getNickName()))
				return false;

			uq.set("nickName", identity.getNickName());
			update = true;
		}

		if (!Strings.isNullOrEmpty(identity.getFirstName())) {
			uq.set("firstName", identity.getFirstName());
			update = true;
		}

		if (!Strings.isNullOrEmpty(identity.getLastName())) {
			update = true;
			uq.set("lastName", identity.getLastName());
		}
		
		LOG.info("Update query: " + uq.toString());

		if (update)
			return ds.update(ds.createQuery(AccountIdentityImpl.class).field("_id").equal(userId), uq)
					.getUpdatedExisting();
		else
			return false;
	}

	@Override
	public boolean checkAccountWithNickName(String nickName) {
		return ds.createQuery(AccountIdentityImpl.class).field("nickName").equal(nickName).getKey() != null;
	}

	@Override
	public boolean updateSetting(String userId, String key, String value) {
		return ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).set("settings." + key, value))
				.getUpdatedExisting();
	}

	@Override
	public boolean updateSettings(String userId, Map<String, String> settings) {
		return ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).set("settings", settings))
				.getUpdatedExisting();
	}

	@Override
	public AccountProfile getProfile(String userId, String wantedProfileId) throws PersistenceException {
		AccountProfile profile = null;

		// Get profile
		if ((profile = ds.get(AccountProfileImpl.class, wantedProfileId)) == null)
			throw new PersistenceException();

		// Filter profile data by some rule
		AccountData acc = getAccountById(userId);
		profile.filterProfile(acc.getGroups().getGroupsList(), acc.getAccountIdentity());

		return profile;
	}

	@Override
	public boolean updateProfile(String userId, AccountProfile profile) throws PersistenceException {
		if (getAccountById(userId, "_id") != null && profile.getIdentity().getId().equals(userId)) {
			ds.save(profile);
			return true;
		}

		return false;
	}

	@Override
	public void addBoard(String userId, Board board) {
		ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId), ds
				.createUpdateOperations(AccountDataImpl.class).add("boards", board));
	}

	@Override
	public void updateBoard(String userId, Board currentBoard) throws PersistenceException {
		removeBoard(userId, currentBoard.getId());
		addBoard(userId, currentBoard);
	}

	@Override
	public boolean removeBoard(String userId, String id) throws PersistenceException {
		return ds
				.getCollection(AccountDataImpl.class)
				.update(new BasicDBObject("_id", userId).append("boards.id", id),
						new BasicDBObject("$pull", new BasicDBObject("boards",
								new BasicDBObject("id", id)))).getN() > 0;
	}

	@Override
	public boolean renameBoard(String userId, String id, String newName) throws PersistenceException {
		return ds
				.getCollection(AccountDataImpl.class)
				.update(new BasicDBObject("_id", userId).append("boards.id", id),
						new BasicDBObject("$set", new BasicDBObject("boards.$.name", newName))).getN() > 0;
	}

	@Override
	public boolean attachGroupToBoard(String userId, String boardId, String groupId) {
		try {
			// Try to get group identity
			getGroupIdentity(groupId);

			// Update
			return ds
					.getCollection(AccountDataImpl.class)
					.update(new BasicDBObject("_id", userId).append("boards.id", boardId),
							new BasicDBObject("$addToSet", new BasicDBObject("boards.$.groups",
									new DBRef(ds.getDB(), GROUP_IDENTITY_COLLECTION, groupId)))).getN() > 0;
		} catch (PersistenceException e) {
			return false;
		}
	}

	@Override
	public boolean detachGroupFromBoard(String userId, String boardId, String groupId) {
		try {
			// Try to get group identity
			getGroupIdentity(groupId);

			// Update
			return ds
					.getCollection(AccountDataImpl.class)
					.update(new BasicDBObject("_id", userId).append("boards.id", boardId),
							new BasicDBObject("$pull", new BasicDBObject("boards.$.groups", new DBRef(ds
									.getDB(), GROUP_IDENTITY_COLLECTION, groupId)))).getN() > 0;
		} catch (PersistenceException e) {
			return false;
		}
	}

	@Override
	public boolean saveGadget(String userId, String boardId, Gadget gadget) throws PersistenceException {
		return removeGadgetFromBoard(userId, boardId, gadget.getGadgetName())
				&& addGadgetToBoard(userId, boardId, gadget);
	}

	@Override
	public boolean addGadgetToBoard(String userId, String boardId, Gadget gadget) {
		return ds
				.getCollection(AccountDataImpl.class)
				.update(new BasicDBObject("_id", userId).append("boards.id", boardId),
						new BasicDBObject("$addToSet", new BasicDBObject("boards.$.gadgets", morphia
								.toDBObject(gadget)))).getN() > 0;
	}

	@Override
	public boolean removeGadgetFromBoard(String userId, String boardId, String gadgetName) {
		return ds
				.getCollection(AccountDataImpl.class)
				.update(new BasicDBObject("_id", userId).append("boards.id", boardId),
						new BasicDBObject("$pull", new BasicDBObject("boards.$.gadgets",
								new BasicDBObject("name", gadgetName)))).getN() > 0;
	}

	@Override
	public boolean isInGroups(String id, String... groups) {
		Query<AccountDataImpl> q = ds.createQuery(AccountDataImpl.class).field("_id").equal(id)
				.disableValidation();
		for (String s : groups) {
			q.field("accountGroups.identity").equal(new GroupIdentityImpl(s));
		}

		return q.countAll() > 0;
	}

	@Override
	public AccountGroup enterToGroup(String userId, String groupId, String answer)
			throws PersistenceException {
		// Get group account
		GroupData g = getGroupById(groupId, "identity", "defaultRole");
		return enterToGroup(userId, g.getIdentity(), answer, g.getRoleContainer().getDefaultRole());
	}

	@Override
	public AccountGroup enterToGroup(String userId, GroupIdentity group, String answer, Role role)
			throws PersistenceException {
		// Check answer
		if (answer != null && !testGroupAnswer(group.getId(), answer))
			return null;

		// Get group account
		AccountGroup ag = new AccountGroupImpl(group, role);

		// Add group id to account identity for optimal searching users by group
		if (ds.update(
				ds.createQuery(AccountIdentityImpl.class).field("_id").equal(userId),

				ds.createUpdateOperations(AccountIdentityImpl.class).disableValidation()
						.add("groups", group.getId(), false)).getUpdatedExisting()

				&& ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
						ds.createUpdateOperations(AccountDataImpl.class).add("accountGroups", ag, false))
						.getUpdatedExisting()

				&& ds.update(ds.createQuery(GroupProfileImpl.class).field("_id").equal(group.getId()),
						ds.createUpdateOperations(GroupProfileImpl.class).inc("users"))
						.getUpdatedExisting())
			return ag;

		return null;

	}

	@Override
	public boolean removeFromGroup(String userId, String groupId) throws PersistenceException {
		AccountData acc = getAccountById(userId, "accountGroups", "boards");
		acc.getGroups().removeFromGroup(groupId);

		if (ds.update(
				ds.createQuery(AccountIdentityImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountIdentityImpl.class).disableValidation()
						.removeAll("groups", groupId)).getUpdatedExisting()
				&& ds.update(
						ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
						ds.createUpdateOperations(AccountDataImpl.class).set("accountGroups",
								acc.getGroups().getGroupsList())).getUpdatedExisting()

				&& ds.update(ds.createQuery(GroupProfileImpl.class).field("_id").equal(groupId),
						ds.createUpdateOperations(GroupProfileImpl.class).inc("users", -1))
						.getUpdatedExisting()) {

			// Detach group from boards
			for (Board b : acc.getBoardsContainer().getBoardsList())
				if (b.hasAttachedGroup(groupId))
					detachGroupFromBoard(userId, b.getId(), groupId);

			return true;
		}

		return false;
	}

	@Override
	public List<AccountIdentity> whosOnline(String userId) throws PersistenceException {
		ContactsContainer contacts = getAccountById(userId, "contacts").getContacts();
		List<AccountIdentity> list = contacts.getAllContacts();

		Iterator<AccountIdentity> it = list.iterator();
		while (it.hasNext()) {
			if (!it.next().isOnline())
				it.remove();
		}

		return list;
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<AccountIdentity> whosOnline(String userId, String groupId) throws PersistenceException {
		// Get online contacts
		List<AccountIdentity> onlineContacts = whosOnline(userId);
		if (onlineContacts.size() == 0)
			return onlineContacts;

		// Create ids list
		Iterator<AccountIdentityImpl> it = ((List<AccountIdentityImpl>) (List<?>) onlineContacts)
				.iterator();
		while (it.hasNext()) {
			if (!it.next().groups.contains(groupId))
				it.remove();
		}

		return onlineContacts;
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<AccountIdentity> whosOnline(String groupId, int start, int perpage) {
		Query<AccountIdentityImpl> q = ds.createQuery(AccountIdentityImpl.class).field("online")
				.equal(true).offset(start).limit(perpage);
		if (groupId != null)
			q.field("groups").equal(groupId);

		return (List<AccountIdentity>) (List<?>) q.asList();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Pair<AccountIdentity, AccountGroup>> getUsersByGroup(String groupId, int start,
			int perpage) {
		Query<AccountDataImpl> acc = ds.createQuery(AccountDataImpl.class).disableValidation()
				.retrievedFields(true, "accountGroups", "identity").field("accountGroups.identity")
				.equal(new GroupIdentityImpl(groupId)).offset(start).limit(perpage);

		List<Pair<AccountIdentity, AccountGroup>> result = new ArrayList<Pair<AccountIdentity, AccountGroup>>();
		for (AccountData d : acc.asList()) {
			result.add(new Pair<AccountIdentity, AccountGroup>(d.getAccountIdentity(), d.getGroups()
					.getAccountGroup(groupId)));
		}

		return result;
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<AccountIdentity> findUsers(String findStr, String groupId, Boolean onlineState,
			int start, int perpage) {
		if (findStr == null || (findStr != null && findStr.trim().equals("")))
			return new ArrayList<AccountIdentity>();

		// Create query
		Query<AccountIdentityImpl> q = ds.createQuery(AccountIdentityImpl.class);

		// Filter by group if needed
		if (groupId != null)
			q.field("groups").equal(groupId);

		// Create query by identity fields
		CriteriaContainer crt = q.or();
		for (String spl : findStr.replaceAll("\\s+", " ").split(" ")) {
			crt.add(q.criteria("firstName").containsIgnoreCase(spl), q.criteria("lastName")
					.containsIgnoreCase(spl), q.criteria("nickName").containsIgnoreCase(spl));
		}

		// Filter by online state if needed
		if (onlineState != null)
			q.and(q.criteria("online").equal(onlineState));

		return (List<AccountIdentity>) (List<?>) q.offset(start).limit(perpage).asList();
	}

	@Override
	public List<AccountProfile> findUsersByProfile(AccountProfile profile, String groupId, int start,
			int perpage) {
		// Sets true in any criteria operation
		// Needs to indicate, that any criteria applied
		boolean anyCrit = false;

		/*
		 * Filter by identity
		 */
		Query<AccountIdentityImpl> qi = ds.createQuery(AccountIdentityImpl.class);
		CriteriaContainer crt = qi.and();

		Iterable<?> byId = null;
		if (profile.getIdentity() != null) {
			anyCrit = true;
			if (profile.getIdentity().getFirstName() != null)
				addTextSearchCriteria(profile.getIdentity().getFirstName(), "firstName", qi, crt);
			if (profile.getIdentity().getLastName() != null)
				addTextSearchCriteria(profile.getIdentity().getLastName(), "lastName", qi, crt);
			if (profile.getIdentity().getNickName() != null)
				addTextSearchCriteria(profile.getIdentity().getNickName(), "nickName", qi, crt);

			byId = qi.fetchKeys();
		}

		/*
		 * Filter by extended data
		 */
		Query<AccountProfileImpl> q = ds.createQuery(AccountProfileImpl.class).field("visible")
				.equal(true).offset(start).limit(perpage).disableValidation();
		crt = q.and();

		// Maker criteria for profile static fields
		if (profile.getDob() != null) {
			anyCrit = true;
			crt.add(q.criteria("dob").equal(profile.getDob()),
					q.criteria("searchRule.items").notEqual(RuleItem.HIDE_DOB));
		}
		if (profile.getGender() != null) {
			anyCrit = true;
			crt.add(q.criteria("gender").equal(profile.getGender()), q.criteria("searchRule.items")
					.notEqual(RuleItem.HIDE_GENDER));
		}
		if (profile.getSocialStatus() != null) {
			anyCrit = true;
			crt.add(q.criteria("socStatus").equal(profile.getSocialStatus()),
					q.criteria("searchRule.items").notEqual(RuleItem.HIDE_SOCIAL_STATUS));
		}
		if (profile.getLittleAbout() != null && !profile.getLittleAbout().trim().equals("")) {
			anyCrit = true;
			addTextSearchCriteria(profile.getLittleAbout(), "about", q, crt);
			crt.add(q.criteria("searchRule.items").notEqual(RuleItem.HIDE_ABOUT));
		}
		if (profile.getStatus() != null && !profile.getStatus().equals("")) {
			anyCrit = true;
			addTextSearchCriteria(profile.getStatus(), "status", q, crt);
			crt.add(q.criteria("searchRule.items").notEqual(RuleItem.HIDE_STATUS));
		}
		if (profile.getConnections().getConnectinsList().size() > 0) {
			crt.add(q.criteria("searchRule.items").notEqual(RuleItem.HIDE_CONNECTIONS));

			for (Connection conn : profile.getConnections().getConnectinsList()) {
				if (conn.getValue().trim().equals(""))
					continue;

				crt.add(q.and(q.criteria("connections.type").equal(conn.getType()),
						q.criteria("connections.value").containsIgnoreCase(conn.getValue().trim())));
				anyCrit = true;
			}
		}

		// Make criteria for extended fields
		if (profile.getProfileAreas().size() > 0) {
			crt.add(q.criteria("searchRule.items").notEqual(RuleItem.HIDE_EXTENDED_PROFILE));

			for (ProfileArea area : profile.getProfileAreas()) {
				if (area.getValue().trim().equals(""))
					continue;

				crt.add(q.and(q.criteria("profileAreas.name").containsIgnoreCase(area.getName()), q
						.criteria("profileAreas.value").containsIgnoreCase(area.getValue().trim())));

				anyCrit = true;
			}
		}

		if (byId != null && byId.iterator().hasNext())
			return filterProfilesForSearch(q.field("identity").in(byId).asList());
		else if (anyCrit)
			return filterProfilesForSearch(q.asList());
		else
			return new ArrayList<AccountProfile>();
	}

	@Override
	public boolean updatePassword(String userId, String oldPassword, String newPassword)
			throws PersistenceException {
		AccountData acc = getAccountById(userId, "salt", "digest");
		Pair<byte[], byte[]> sd = acc.getSaltDigest();
		PasswordDigest digest = new PasswordDigest(sd.getFirst(), sd.getSecond());
		if (!digest.verify(oldPassword.toCharArray()))
			return false;

		// Create new password
		digest = new PasswordDigest(newPassword.toCharArray());

		return ds.update(
				ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).set("salt", digest.getSalt())
						.set("digest", digest.getDigest())).getUpdatedExisting();
	}

	/*
	 * Contacts groups
	 */
	@Override
	public boolean contactsOwnedBy(String userId, String groupId) {
		return ds.createQuery(AccountDataImpl.class).field("_id").equal(userId).disableValidation()
				.field("contacts").equal(new ContactsGroupImpl(groupId)).countAll() > 0;
	}

	@Override
	public ContactsGroup getContactsGroup(String cgroupId) throws PersistenceException {
		ContactsGroup g = ds.get(ContactsGroupImpl.class, cgroupId);

		if (g == null)
			throw new PersistenceException();

		return g;
	}

	@Override
	public boolean addContactsGroup(String userId, ContactsGroup group) {
		ds.save(group);
		return ds.update(ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).add("contacts", group))
				.getUpdatedExisting();
	}

	@Override
	public boolean removeContactsGroup(String userId, String groupId) {
		return ds.update(
				ds.createQuery(AccountDataImpl.class).field("_id").equal(userId),
				ds.createUpdateOperations(AccountDataImpl.class).removeAll("contacts",
						new ContactsGroupImpl(groupId))).getUpdatedExisting()
				&& ds.delete(ContactsGroupImpl.class, groupId).getN() > 0;
	}

	@Override
	public boolean renameContactsGroup(String groupId, String newName) {
		return ds.update(ds.createQuery(ContactsGroupImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(ContactsGroupImpl.class).set("name", newName))
				.getUpdatedExisting();
	}

	@Override
	public boolean addContactToGroup(String groupId, AccountIdentity account) {
		return ds.update(ds.createQuery(ContactsGroupImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(ContactsGroupImpl.class).add("users", account))
				.getUpdatedExisting();
	}

	@Override
	public boolean removeContactFromGroup(String groupId, String... contactIds)
			throws PersistenceException {
		ContactsGroup g = getContactsGroup(groupId);

		List<AccountIdentity> ids = new ArrayList<AccountIdentity>();
		for (String id : contactIds)
			ids.add(new AccountIdentityImpl(id));

		return ds.update(ds.createQuery(ContactsGroupImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(ContactsGroupImpl.class).removeAll("users", ids))
				.getUpdatedExisting();
	}

	@Override
	public boolean applyRoleToUser(String roleId, String userId, String groupId)
			throws PersistenceException {
		// Try to get role with given id
		return ds.get(RoleImpl.class, roleId) != null
				&&

				// Update account data
				ds.update(
						ds.createQuery(AccountDataImpl.class).field("_id").equal(userId)
								.disableValidation().field("accountGroups.identity")
								.equal(new Key<GroupIdentity>(GroupIdentityImpl.class, groupId)),
						ds.createUpdateOperations(AccountDataImpl.class).disableValidation()
								.set("accountGroups.$.role", new Key<Role>(RoleImpl.class, roleId)))
						.getUpdatedExisting();
	}

	/*
	 * GroupStore
	 */
	@Override
	public boolean testGroupAnswer(String groupId, String answer) {
		return ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId).field("testAnsw")
				.equal(answer).countAll() > 0;
	}

	@Override
	public boolean createGroup(GroupData group) throws PersistenceException, NameExistsException {
		// We can't process already existing group
		if (ds.exists(group.getIdentity()) != null)
			return false;

		// Check number of roles
		if (group.getRoleContainer().getRoleList().size() == 0)
			throw new PersistenceException();

		// Set first role as default if needed
		if (group.getRoleContainer().getDefaultRole() == null)
			group.getRoleContainer().setDefaultRole( group.getRoleContainer().getRole(0) );

		if (ds.createQuery(GroupIdentityImpl.class).field("alias").equal(group.getIdentity().getAlias())
				.getKey() != null)
			throw new NameExistsException();

		// Try to save identity
		ds.save(group.getIdentity());

		// Save profile
		ds.save(group.getProfile());

		// Save roles
		for (Role role : group.getRoleContainer().getRoleList()) {			
			String newId = new ObjectId().toString();

			// If this default role set it in container
			if( group.getRoleContainer().getDefaultRole().equals( role ) )
				group.getRoleContainer().setDefaultRole(role);
			
			role.setId(newId);
			createRole(role);
		}

		// Save data
		ds.save(group);
		return true;
	}

	@Override
	public boolean updateTestQuest(String groupId, String quest, String answ) {
		return ds.update(
				ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(GroupDataImpl.class).set("testQuest", quest)
						.set("testAnsw", answ)).getUpdatedExisting();
	}

	@Override
	public boolean updateProfile(GroupProfile profile) {
		return ds.updateFirst(ds.createQuery(GroupProfileImpl.class).field("_id").equal(profile.getId()),
				(GroupProfileImpl) profile, false).getUpdatedExisting();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<GroupProfile> findGroup(String name, int offset, int limit) {
		if (name == null || (name != null && name.trim().equals("")))
			return new ArrayList<GroupProfile>();

		Query<GroupIdentityImpl> q = ds.createQuery(GroupIdentityImpl.class);
		CriteriaContainer crt = q.or();

		for (String spl : name.replaceAll("\\s+", " ").split(" ")) {
			crt.add(q.criteria("name").containsIgnoreCase(spl),
					q.criteria("alias").containsIgnoreCase(spl));
		}

		List<?> lst = q.offset(offset).limit(limit).asKeyList();

		if (lst.size() == 0)
			return new ArrayList<GroupProfile>();

		return (List<GroupProfile>) (List<?>) ds.createQuery(GroupProfileImpl.class).field("identity")
				.in(lst).asList();
	}
	
	@Override
	public List<GroupProfile> getAllGroups(int offset, int limit) {
		return (List<GroupProfile>) (List<?>) ds.createQuery(GroupProfileImpl.class)
					.order("-users").offset(offset).limit(limit).asList();
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<GroupProfile> findGroupByProfile(GroupProfile profile, int offset, int limit) {
		boolean anyCrit = false;

		/*
		 * Filter by identity
		 */
		Query<GroupIdentityImpl> qi = ds.createQuery(GroupIdentityImpl.class);
		CriteriaContainer crt = qi.and();

		Iterable<?> byId = null;
		if (profile.getIdentity() != null) {
			if (isNotEmpty(profile.getIdentity().getAlias())) {
				crt.add(qi.criteria("alias").containsIgnoreCase(profile.getIdentity().getAlias().trim()));
			}
			if (isNotEmpty(profile.getIdentity().getPrintName())) {
				addTextSearchCriteria(profile.getIdentity().getPrintName().trim(), "name", qi, crt);
			}

			byId = qi.fetchKeys();
		}

		/*
		 * Filter by extended data
		 */
		Query<GroupProfileImpl> q = ds.createQuery(GroupProfileImpl.class).offset(offset).limit(limit)
				.disableValidation();
		crt = q.or();

		// Maker criteria for description
		if (isNotEmpty(profile.getDescription())) {
			anyCrit = true;

			for (String spl : profile.getDescription().trim().replaceAll("\\s+", " ").split(" ")) {
				crt.add(q.criteria("desc").containsIgnoreCase(spl));
			}
		}

		// Make criteria for extended fields
		for (ProfileArea area : profile.getProfileAreas()) {
			if (!isNotEmpty(area.getName()) || !isNotEmpty(area.getValue()))
				continue;
			crt.add(q.and(q.criteria("profileAreas.name").containsIgnoreCase(area.getName().trim()), q
					.criteria("profileAreas.value").containsIgnoreCase(area.getValue().trim())));

			anyCrit = true;
		}

		// For other fields
		if (isNotEmpty(profile.getHomeUrl())) {
			anyCrit = true;
			crt.add(q.criteria("homeUrl").containsIgnoreCase(profile.getHomeUrl().trim()));
		}
		if (profile.getOpenness() != null) {
			anyCrit = true;
			crt.add(q.criteria("openness").equal(profile.getOpenness()));
		}
		if (profile.getNumberOfUsers() < 0) {
			anyCrit = true;
			crt.add(q.criteria("users").lessThanOrEq(profile.getNumberOfUsers() * (-1)));
		} else if (profile.getNumberOfUsers() > 0) {
			anyCrit = true;
			crt.add(q.criteria("users").greaterThanOrEq(profile.getNumberOfUsers()));
		}

		if (byId != null && byId.iterator().hasNext())
			return (List<GroupProfile>) (List<?>) q.field("identity").in(byId).asList();
		else if (anyCrit)
			return (List<GroupProfile>) (List<?>) q.asList();
		else
			return new ArrayList<GroupProfile>();
	}

	@Override
	public GroupData getGroupById(String id) throws PersistenceException {
		GroupData data = null;

		if ((data = ds.get(GroupDataImpl.class, id)) == null)
			throw new PersistenceException();

		return data;
	}

	@Override
	public GroupData getGroupById(String id, String... fields) throws PersistenceException {
		GroupData group = ds.createQuery(GroupDataImpl.class).field("_id").equal(id)
				.retrievedFields(true, fields).get();

		if (group == null)
			throw new PersistenceException();

		return group;
	}

	@Override
	public GroupProfile getGroupProfile(String id) throws PersistenceException {
		GroupProfile profile = null;

		if ((profile = ds.get(GroupProfileImpl.class, id)) == null)
			throw new PersistenceException();

		return profile;
	}

	@Override
	public GroupIdentity getGroupIdentity(String id) throws PersistenceException {
		GroupIdentity identity = null;

		if ((identity = ds.get(GroupIdentityImpl.class, id)) == null)
			throw new PersistenceException();

		return identity;
	}

	@Override
	public boolean updateRole(Role role) {
		return ds.updateFirst(ds.createQuery(RoleImpl.class).field("_id").equal(role.getId()),
				(RoleImpl) role, false).getUpdatedExisting();
	}

	@Override
	public void createRole(Role role) {
		ds.save(role);
	}

	@Override
	public Role getRole(String roleId, String groupId) {
		return ds.find(RoleImpl.class).field("_id").equal(roleId).get();
	}

	@Override
	public void removeGroup(String id) throws PersistenceException {
		List<Role> roles = getGroupById(id, "roles").getRoleContainer().getRoleList();

		ds.delete(GroupDataImpl.class, id);
		ds.delete(roles);
		ds.delete(GroupIdentityImpl.class, id);
		ds.delete(GroupProfileImpl.class, id);
	}

	@Override
	public boolean addRole(String groupId, Role role) {
		createRole(role);

		return ds.update(ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(GroupDataImpl.class).add("roles", role)).getUpdatedExisting();
	}

	@Override
	public boolean removeRole(String groupId, Role role) throws PersistenceException {
		// Get group
		GroupData group = getGroupById(groupId, "roles", "defaultRole");

		if (group == null)
			throw new PersistenceException();

		RoleContainer roles = group.getRoleContainer();

		// Group can't contain one role
		if (group.getRoleContainer().getRoleList().size() == 1)
			return false;

		// Update default role
		if (group.getRoleContainer().getDefaultRole().equals(role)) {
			// How many roles have this group
			roles.getRoleList().remove(role);

			if (roles.getRoleList().size() == 0)
				return false;

			// Set as default the first role of the list
			roles.setDefaultRole(roles.getRoleList().get(0));
			setDefaultRole(groupId, roles.getRoleList().get(0));
		}

		// Update users, who have this role. Set his role to default
		ds.getCollection(AccountDataImpl.class).updateMulti(
				new BasicDBObject("accountGroups.role", new DBRef(ds.getDB(), ACCOUNT_ROLES_COLLECTION,
						role.getId())),
				new BasicDBObject("$set", new BasicDBObject("accountGroups.$.role", new DBRef(ds.getDB(),
						ACCOUNT_ROLES_COLLECTION, roles.getDefaultRole().getId()))));

		// Remove role from references
		ds.update(ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(GroupDataImpl.class).removeAll("roles", role))
				.getUpdatedExisting();

		// Delete role
		return ds.delete(role).getN() > 0;
	}

	@Override
	public boolean setDefaultRole(String groupId, Role role) {
		return ds.update(ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(GroupDataImpl.class).set("defaultRole", role))
				.getUpdatedExisting();
	}

	@Override
	public List<GroupEnterRequest> getEnterRequests(String groupId, int offset, int limit) {
		BasicDBList list = new BasicDBList();
		list.put(0, offset);
		list.put(1, limit);

		return morphia.fromDBObject(
				GroupDataImpl.class,
				ds.getCollection(GroupDataImpl.class).findOne(new BasicDBObject("_id", groupId),
						new BasicDBObject("requests", new BasicDBObject("$slice", list))))
				.getEnterRequests();
	}

	@Override
	public boolean addEnterRequest(String groupId, GroupEnterRequest request) {
		Preconditions.checkNotNull(groupId, "null group id");
		Preconditions.checkNotNull(request, "null request");

		// Check if user already in group
		if (isInGroups(request.getCondidate().getId(), groupId))
			return false;

		// Check if user already send request
		if (ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId).disableValidation()
				.field("requests.condidate").equal(ds.getKey(request.getCondidate())).getKey() != null)
			return false;

		// Send request
		return ds.update(ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(GroupDataImpl.class).add("requests", request))
				.getUpdatedExisting();
	}

	@Override
	public boolean processEnterRequest(String groupId, String accountId) throws PersistenceException {
		Preconditions.checkNotNull(groupId);
		Preconditions.checkNotNull(accountId);

		// Check group contain request for given account id
		if (ds.createQuery(GroupDataImpl.class).field("_id").equal(groupId).disableValidation()
				.field("requests.condidate")
				.equal(new Key<AccountIdentityImpl>(AccountIdentityImpl.class, accountId)).getKey() == null)
			return false;

		// Enter account to group
		return enterToGroup(accountId, groupId, null) != null && removeEnterRequest(groupId, accountId);
	}

	@Override
	public boolean removeEnterRequest(String groupId, String accountId) {
		DBRef ref = new DBRef(ds.getDB(), ACCOUNT_IDENTITY_COLLECTION, accountId);

		return ds
				.getCollection(GroupDataImpl.class)
				.update(new BasicDBObject("_id", groupId).append("requests.condidate", ref),
						new BasicDBObject("$pull", new BasicDBObject("requests", new BasicDBObject(
								"condidate", ref)))).getN() > 0;
	}

	@Override
	public List<GroupProfile> getGroupsByIds(Iterable<String> ids) {
		return (List<GroupProfile>) (List<?>) ds.get(GroupProfileImpl.class, ids).asList();
	}

	@Override
	public boolean updatePrintName(String groupId, String newName) {
		return ds.update(ds.createQuery(GroupIdentityImpl.class).field("_id").equal(groupId),
				ds.createUpdateOperations(GroupIdentityImpl.class).set("name", newName))
				.getUpdatedExisting();
	}

	/*
	 * Confirm store
	 */
	@Override
	public ConfirmEntry getConfirmEntry(String confirm) throws PersistenceException {
		ConfirmEntry entry = ds.get(ConfirmEntry.class, confirm);

		if (entry == null)
			throw new PersistenceException();

		return entry;
	}

	@Override
	public void addConfirmEntry(ConfirmEntry entry) {
		ds.save(entry);
	}

	@Override
	public boolean removeConfirmEntry(String confirm) {
		return ds.delete(ConfirmEntry.class, confirm).getN() > 0;
	}

	/*
	 * Useful methods
	 */
	@SuppressWarnings("unchecked")
	private List<AccountProfile> filterProfilesForSearch(List<AccountProfileImpl> profiles) {
		List<AccountProfile> filtred = (List<AccountProfile>) (List<?>) profiles;

		for (AccountProfile p : filtred)
			p.filterProfileForSearch();

		return filtred;
	}

	private void addTextSearchCriteria(String str, String field, Query<?> q, CriteriaContainer crt_and) {
		CriteriaContainer crt = q.or();
		for (String spl : str.replaceAll("\\s+", " ").split(" "))
			crt.add(q.criteria(field).containsIgnoreCase(spl));

		crt_and.add(crt);
	}

	private boolean isNotEmpty(String str) {
		return str != null && str.trim().length() > 0;
	}

	/*
	 * Attachment
	 */
	private GridFS getAttachmentGrid() {
		if (attachmentGrid == null) {
			attachmentGrid = new GridFS(ds.getDB(), ATTACH_COLLECTION);
		}

		return attachmentGrid;
	}

	@Override
	public AttachmentStreamer getAttachment(String id) {
		LOG.info("Get attachment with id: " + id);
		final GridFSDBFile attachment = getAttachmentGrid().findOne(new BasicDBObject("_id", id));

		if (attachment == null) {
			LOG.info("\tNot found");
			return null;
		} else {
			return new AttachmentStreamer() {
				@Override
				public void writeDataTo(OutputStream out) throws IOException {
					attachment.writeTo(out);
				}

				@Override
				public Date getLastModifiedDate() {
					return attachment.getUploadDate();
				}

				@Override
				public long getContentSize() {
					return attachment.getLength();
				}

				@Override
				public InputStream getInputStream() {
					return attachment.getInputStream();
				}

				@Override
				public String getMimeType() {
					return attachment.getContentType();
				}

				@Override
				public String getFileName() {
					return attachment.getFilename();
				}

				@Override
				public String getId() {
					return attachment.getId().toString();
				}
			};
		}
	}

	@Override
	public String storeAttachment(InputStream data, String fileName) throws IOException,
			FileSizeLimitException {
		// Limit file size
		InputStream stream = new LimitedInputStream(data, maxFileSize) {
			@Override
			protected void raiseError(long arg0, long arg1) throws IOException {
				throw new IOException();
			}
		};

		// Create file
		GridFSInputFile file = getAttachmentGrid().createFile(stream);

		// Set file data
		ObjectId id = new ObjectId();
		file.put("_id", id.toString());
		file.put("notUsed", true);
		file.setFilename(fileName);

		// Save file
		try {
			file.save();
			return id.toString();
		} catch (MongoException e) {
			Throwable innerException = e.getCause();

			if (innerException instanceof IOException) {
				LOG.info("File seize limit exception: ", e);
				throw new FileSizeLimitException();
			} else {
				LOG.error("Error while saving an attachment: ", e);
				throw e;
			}
		}

	}

	@Override
	public void removeAttachmentFully(String id) throws PersistenceException {
		ds.delete(AttachmentIdentityImpl.class, id);
		ds.delete(AttachmentInfoImpl.class, id);
		getAttachmentGrid().remove(new BasicDBObject("_id", id));
	}

	public void removeAttachmentData(String id) {
		getAttachmentGrid().remove(new BasicDBObject("_id", id));
	}

	@Override
	public void incrementDownloads(String id) {
		ds.update(ds.createQuery(AttachmentIdentityImpl.class).field("_id").equal(id), ds
				.createUpdateOperations(AttachmentIdentityImpl.class).inc("downloads"));
	}

	@Override
	public AttachmentInfo getAttachmentInfo(String id) throws PersistenceException {
		AttachmentInfo data = ds.get(AttachmentInfoImpl.class, id);

		if (data == null)
			throw new PersistenceException();

		return data;
	}

	@Override
	public AttachmentInfo getAttachmentInfo(String id, String... fields) throws PersistenceException {
		AttachmentInfo data = ds.createQuery(AttachmentInfoImpl.class).field("_id").equal(id)
				.retrievedFields(true, fields).get();

		if (data == null)
			throw new PersistenceException();

		return data;
	}

	@Override
	public AttachmentIdentity getAttachmentIdentity(String id) throws PersistenceException {
		AttachmentIdentity identity = ds.get(AttachmentIdentityImpl.class, id);

		if (identity == null)
			throw new PersistenceException();

		return identity;
	}

	@Override
	public void saveAttachmentIdentity(AttachmentIdentity second) {
		// Remove "not used" marker
		ds.getDB().getCollection(ATTACH_COLLECTION)
				.update(new BasicDBObject("_id", second.getId()), new BasicDBObject("$unset", "notUsed"));

		// Save identity
		ds.save(second);
	}

	@Override
	public void removeNotUsed() {
		getAttachmentGrid().remove(new BasicDBObject("notUsed", true));
	}

	@Override
	public void saveAttachmentData(AttachmentInfo first) {
		ds.save(first);
	}

	@Override
	public void replaceAttachment(String id, InputStream data) throws PersistenceException {
		AttachmentStreamer att = getAttachment(id);

		if (att != null) {
			// Delete old file
			removeAttachmentData(id);

			// Save new file
			GridFSInputFile file = getAttachmentGrid().createFile(data);
			file.put("_id", id);
			file.setFilename(att.getFileName());
			file.save();
		}
	}

	/*
	 * Document store
	 */
	@Override
	public DocumentData getDocument(String id) throws PersistenceException {
		DocumentData doc = morphia.fromDBObject(
				DocumentDataImpl.class,
				ds.getCollection(DocumentDataImpl.class).findOne(new BasicDBObject("_id", id),
						new BasicDBObject("discussion.messages", new BasicDBObject("$slice", -20))));

		if (doc == null)
			throw new PersistenceException();

		return doc;
	}

	@Override
	public DocumentData getDocument(String id, String... fields) throws PersistenceException {
		DocumentData doc = ds.createQuery(DocumentDataImpl.class).field("_id").equal(id)
				.retrievedFields(true, fields).get();

		if (doc == null)
			throw new PersistenceException();

		return doc;
	}

	@Override
	public List<DocumentData> getDocuments(int offset, int perpage) {
		return getDocuments(offset, perpage, null, null);
	}

	@Override
	public List<DocumentData> getDocuments(int offset, int perpage, DocumentType... types) {
		return getDocuments(offset, perpage, null, Arrays.asList(types));
	}

	@Override
	public List<DocumentData> getDocuments(int offset, int perpage, Iterable<String> groupId,
			Iterable<DocumentType> types) {		
		// Create query
		Query q = ds.createQuery(DocumentIdentityImpl.class).field("trash").equal(false)
				.order("-updateDate").offset(offset).limit(perpage);

		// If we need filter by types
		if (types != null)
			q.field("types").hasAnyOf(types);

		// Or by groups
		if (groupId != null) {
			List<Key<GroupIdentity>> gids = new LinkedList<Key<GroupIdentity>>();
			for (String id : groupId)
				gids.add(new Key(GroupIdentityImpl.class, id));

			q.field("group").hasAnyOf(gids);
		}

		// Retrive keys of results 
		List<String> keyResult = new ArrayList<String>();
		for(Key<DocumentIdentityImpl> key : (Iterable<Key<DocumentIdentityImpl>>)q.fetchKeys())
			keyResult.add( key.getId().toString() );

		if (keyResult.size() == 0)
			return new ArrayList<DocumentData>();

		// Get documents with cut discussion
		DBCursor cursor = ds.getCollection(DocumentDataImpl.class).find(
				new BasicDBObject("_id", new BasicDBObject("$in", keyResult )),
				new BasicDBObject("discussion.messages", new BasicDBObject("$slice", -20))
				).sort(new BasicDBObject("updateDate", -1));
		
		// Get result converted to POJO
		List<DocumentData> result = new ArrayList<DocumentData>();
		while(cursor.hasNext())
			result.add( morphia.fromDBObject(DocumentDataImpl.class, cursor.next()) );
		
		return result;
	}

	@Override
	// TODO: Test it
	public int getRecentDocumentsCount(Date startDate, Iterable<String> groupId,
			Iterable<DocumentType> types) {
		// Create query
		Query q = ds.createQuery(DocumentIdentityImpl.class).field("trash").equal(false)
				.field("updateDate").greaterThanOrEq(startDate);

		// If we need filter by types
		if (types != null)
			q.field("types").hasAllOf(types);

		// Or by groups
		if (groupId != null) {
			List<Key<GroupIdentity>> gids = new ArrayList<Key<GroupIdentity>>();
			for (String id : groupId)
				gids.add(new Key(GroupIdentityImpl.class, id));

			q.field("group").hasAnyOf(gids);
		}

		return (int) q.countAll();
	}

	/**
	 * Cast nodes to their class
	 * 
	 * @param cur
	 * @return
	 */
	private List<NodeData> getCastedNodes(DBCursor cur) {
		List<NodeData> nodes = new ArrayList<NodeData>();

		while (cur.hasNext())
			nodes.add(getCastedNode(cur.next()));

		return nodes;
	}

	private NodeData getCastedNode(DBObject obj) {
		if (obj == null)
			return null;

		if (obj.get("className").equals(NodeDataImpl.class.getCanonicalName()))
			return morphia.fromDBObject(TreeNode.class, obj);
		else
			return morphia.fromDBObject(DocumentNode.class, obj);
	}
	
	@Override
	public List<NodeData> getNodes(String groupId, List<String> nodeIds) {
		return getCastedNodes(ds.getCollection(NodeDataImpl.class).find(
				new BasicDBObject("_id", new BasicDBObject("$in", nodeIds)).append("groupId", groupId)));		
	}

	@Override
	public List<NodeData> getRootTree(String groupId) {
		return getCastedNodes(ds.getCollection(NodeDataImpl.class).find(
				new BasicDBObject("groupId", groupId).append("parentId", null).append("trash", false)));
	}

	@Override
	public List<NodeData> getSubTree(String groupId, String nodeId) {
		return getCastedNodes(ds.getCollection(NodeDataImpl.class).find(
				new BasicDBObject("groupId", groupId).append("parentId", nodeId).append("trash", false)));
	}

	@Override
	public List<NodeData> getSubTreeByPath(String groupId, String path) {
		return getCastedNodes(ds.getCollection(NodeDataImpl.class).find(
				new BasicDBObject("groupId", groupId).append("path", path).append("trash", false)));
	}

	@Override
	public NodeData getNodeByPath(String path, String nodeName, String groupId) {
		return getCastedNode(ds.getCollection(NodeDataImpl.class).findOne(
				new BasicDBObject("path", path).append("name", nodeName).append("groupId", groupId)
						.append("trash", false)));
	}

	@Override
	public List<NodeData> getAllSubTreesByPath(String groupId, String path) {
		return getCastedNodes(ds.getCollection(NodeDataImpl.class).find(
				new BasicDBObject("groupId", groupId).append("path",
						Pattern.compile("^" + path, Pattern.CASE_INSENSITIVE)).append("trash", false)));
	}

	@Override
	public List<NodeData> getTrash(String groupId) {
		return getCastedNodes(ds.getCollection(NodeDataImpl.class).find(
				new BasicDBObject("groupId", groupId).append("trash", true)));
	}

	@Override
	public List<NodeData> getSubTreesToPath(String groupId, String nodeId) throws PersistenceException {
		// Try to get node
		NodeData node = getNode(nodeId);
		BasicDBObject query = new BasicDBObject("groupId", groupId).append("trash", false);
		BasicDBList or = new BasicDBList();

		// Create query
		String path = node.getPath();
		String[] paths = path.split("/");

		if (!"".equals(path)) {
			StringBuilder pathBuff = new StringBuilder("");

			for (String p : paths) {
				pathBuff.append(p + "/");
				or.add(new BasicDBObject("path", Pattern.compile("^" + Pattern.quote(pathBuff.toString())
						+ "$", Pattern.CASE_INSENSITIVE)));
			}
		}

		// Get nodes
		return getCastedNodes(ds.getCollection(NodeDataImpl.class).find(query.append("$or", or)));
	}

	@Override
	public void saveNode(TreeNode node) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(node.getGroupId()));

		// Save node
		ds.save(node);
	}

	@Override
	public boolean checkNodeWithNameExisting(String groupId, String parentId, String name) {
		return ds.getCollection(NodeDataImpl.class).findOne(
				new BasicDBObject("groupId", groupId).append("trash", false).append("parentId", parentId)
						.append("name", name)) != null;
	}

	@Override
	public boolean renameNode(String groupId, String nodeId, String newName) throws PersistenceException {
		// Get node for getting children
		NodeData node = getNode(nodeId, groupId);
		if (!(node instanceof TreeNode))
			return false;

		if (node.getName().equals(newName))
			return true;

		if (checkNodeWithNameExisting(groupId, node.getParentId(), newName))
			return false;

		// Get children
		List<NodeData> children = getAllSubTreesByPath(node.getGroupId(), node.getPathForChildren());

		// Get paths
		String oldPath = node.getPathForChildren();
		((TreeNode) node).setName(newName);
		String newPath = node.getPathForChildren();

		// Update children path
		for (NodeData child : children) {
			ds.update(
					ds.createQuery(NodeDataImpl.class).field("_id").equal(child.getId()),
					ds.createUpdateOperations(NodeDataImpl.class).set("path",
							child.getPath().replace(oldPath, newPath)));
		}

		return ds.update(
				ds.createQuery(NodeDataImpl.class).field("_id").equal(nodeId).field("groupId")
						.equal(groupId),
				ds.createUpdateOperations(NodeDataImpl.class).set("name", newName)).getUpdatedExisting();
	}

	@Override
	public boolean removeNode(String groupId, String nodeId) throws PersistenceException {
		return removeNode(groupId, Arrays.asList(nodeId));
	}

	@Override
	public boolean removeNode(String groupId, List<String> nodeIds) throws PersistenceException {
		// Remove each node
		for (String nodeId : nodeIds) {
			// Get node for getting children
			NodeData node = getNode(nodeId, groupId);

			if (node.getPathForChildren() != null) {
				List<NodeData> children = getAllSubTreesByPath(node.getGroupId(),
						node.getPathForChildren());

				// Remove children
				for (NodeData child : children) {
					if (child instanceof DocumentNode)
						removeDocument(((DocumentNode) child).getDocumentIdentity().getId());

					ds.delete(ds.createQuery(NodeDataImpl.class).field("_id").equal(child.getId()));
				}
			}

			// Remove itself
			if (ds.delete(
					ds.createQuery(NodeDataImpl.class).field("_id").equal(nodeId).field("groupId")
							.equal(groupId)).getN() == 0)
				return false;
		}

		return true;
	}

	@Override
	public boolean removeDocument(String id) throws PersistenceException {
		DocumentData doc = getDocument(id, "attach", "diffs", "discussion", "node");

		// Delete attachments
		for (AttachmentIdentity att : doc.getAttachments().getAttachmentList())
			removeAttachmentFully(att.getId());

		// Delete patches
		for (DocumentDiff d : doc.getDiffs())
			ds.delete(DocumentPatchImpl.class, d.getPatchId());

		// Delete document
		return removeNode(doc.getNode().getGroupId(), doc.getNode().getId())
				&& ds.delete(DocumentIdentityImpl.class, id).getN() > 0
				&& ds.delete(DocumentDataImpl.class, id).getN() > 0;
	}

	@Override
	public void moveNodes(String groupId, String newParentNode, String... nodes)
			throws PersistenceException {
		// Get node where we move
		NodeData toNode = getNode(newParentNode, groupId);

		for (String nodeId : nodes) {
			// Get moving node and his children
			NodeData node = getNode(nodeId);
			List<NodeData> children = getAllSubTreesByPath(node.getGroupId(), node.getPathForChildren());

			// Check group id
			if (!node.getGroupId().equals(toNode.getGroupId()))
				continue;

			// Move itself
			ds.update(ds.createQuery(NodeDataImpl.class).field("_id").equal(node.getId()), ds
					.createUpdateOperations(NodeDataImpl.class).set("path", toNode.getPathForChildren())
					.set("parentId", newParentNode));

			// Paths for replace
			String oldPath = node.getPathForChildren();
			node.setPath(toNode.getPathForChildren());
			String newPath = node.getPathForChildren();

			// Move children
			for (NodeData child : children) {
				ds.update(
						ds.createQuery(NodeDataImpl.class).field("_id").equal(child.getId()),
						ds.createUpdateOperations(NodeDataImpl.class).set("path",
								child.getPath().replace(oldPath, newPath)));
			}
		}
	}

	@Override
	public NodeData getNode(String id) throws PersistenceException {
		NodeData node = getCastedNode(ds.getCollection(NodeDataImpl.class).findOne(id));

		if (node == null)
			throw new PersistenceException();

		return node;
	}

	private NodeData getNode(String nodeId, String groupId) throws PersistenceException {
		NodeData node = getCastedNode(ds.getCollection(NodeDataImpl.class).findOne(
				new BasicDBObject("_id", nodeId).append("groupId", groupId)));

		if (node == null)
			throw new PersistenceException();

		return node;
	}

	@Override
	public void putToTrash(String groupId, String... nodes) throws PersistenceException {
		for (String nodeId : nodes) {
			// Get moving node and his children
			NodeData node = getNode(nodeId, groupId);
			List<NodeData> children = getAllSubTreesByPath(node.getGroupId(), node.getPathForChildren());
			children.add(node);

			// Update nodes
			for (NodeData child : children) {
				ds.update(ds.createQuery(NodeDataImpl.class).field("_id").equal(child.getId()), ds
						.createUpdateOperations(NodeDataImpl.class).set("trash", true));

				if (child instanceof DocumentNode)
					ds.update(
							ds.createQuery(DocumentIdentityImpl.class).field("_id")
									.equal(((DocumentNode) child).getDocumentIdentity().getId()), ds
									.createUpdateOperations(DocumentIdentityImpl.class)
									.set("trash", true));
			}
		}
	}

	@Override
	public DocumentPatch getDocumentPatch(String id) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(id));
		return ds.get(DocumentPatchImpl.class, id);
	}

	@Override
	public List<DocumentPatch> getDocumentPatchs(List<String> neededPatchs) {
		return (List<DocumentPatch>) (List<?>) ds.createQuery(DocumentPatchImpl.class).field("_id")
				.in(neededPatchs).asList();
	}

	@Override
	public void createPatch(DocumentPatch p) {
		ds.save(p);
	}

	@Override
	public DocumentIdentity getDocumentIdentity(String id) throws PersistenceException {
		DocumentIdentity idnt = ds.get(DocumentIdentityImpl.class, id);
		
		if(idnt == null)
			throw new PersistenceException();
		
		return idnt;
	}

	@Override
	public List<DocumentData> findDocuments(String str, Iterable<String> groupId, String authorId,
			Iterable<DocumentType> types, int offset, int limit) {
		if (!isNotEmpty(str))
			return new ArrayList<DocumentData>();

		// Cerate query
		Query<DocumentIdentityImpl> q = ds.createQuery(DocumentIdentityImpl.class).offset(offset)
				.limit(limit).disableValidation();
		CriteriaContainer crt = q.or();

		// Filter by name of group
		for (String spl : str.trim().replaceAll("\\s+", " ").split(" "))
			crt.add(q.criteria("name").containsIgnoreCase(spl));

		// If we need filter by types
		if (types != null)
			q.field("types").hasAllOf(types);

		// Or by groups
		if (groupId != null) {
			List<Key<GroupIdentity>> gids = new ArrayList<Key<GroupIdentity>>();
			for (String id : groupId)
				gids.add(new Key(GroupIdentityImpl.class, id));

			q.field("group").hasAnyOf(gids);
		}

		// Filter by author
		if (authorId != null)
			q.field("author").equal(new Key<AccountIdentity>(AccountIdentityImpl.class, authorId));
		
		// Fetch keys
		List<String> keyResult = new ArrayList<String>();
		for(Key<DocumentIdentityImpl> key : (Iterable<Key<DocumentIdentityImpl>>)q.fetchKeys())
			keyResult.add( key.getId().toString() );

		if (keyResult.size() == 0)
			return new ArrayList<DocumentData>();

		// Get documents with cut discussion
		DBCursor cursor = ds.getCollection(DocumentDataImpl.class).find(
				new BasicDBObject("_id", new BasicDBObject("$in", keyResult )),
				new BasicDBObject("discussion.messages", new BasicDBObject("$slice", -5))
				);
		
		// Get result converted to POJO
		List<DocumentData> result = new ArrayList<DocumentData>();
		while(cursor.hasNext())
			result.add( morphia.fromDBObject(DocumentDataImpl.class, cursor.next()) );
		
		return result;
	}

	@Override
	public List<DocumentData> findDocumentsByProfile(String groupId, String authorId, DocumentType type,
			ProfileContainer profile, int offset, int limit) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public void createDocument(DocumentData doc) {
		Preconditions.checkNotNull(doc.getNode());

		// Set date
		Date creationDate = new Date();
		doc.setCreationDate(creationDate);
		doc.setUpdateDate(creationDate);

		ds.save(doc.getIdentity()).getId();
		ds.save(doc.getNode());
		ds.save(doc);
	}

	@Override
	public boolean changeDocument(String docId, DocumentData doc) {
		// Get keys for attachments :(
		List<Key<AttachmentIdentity>> attIdnt = new ArrayList<Key<AttachmentIdentity>>();
		for (AttachmentIdentity id : doc.getAttachments().getAttachmentList())
			attIdnt.add(ds.getKey(id));

		// Update identity
		return ds.update(
				ds.createQuery(DocumentIdentityImpl.class).disableValidation().field("_id")
						.equal(doc.getIdentity().getId()).field("author")
						.equal(doc.getIdentity().getAuthor()),
				ds.createUpdateOperations(DocumentIdentityImpl.class)
						.set("types", doc.getIdentity().getDocumentTypes().getTypesList())
						.set("updateDate", new Date())
						.set("name", doc.getIdentity().getName()))
				.getUpdatedExisting()
				&&
				// Update data
				ds.update(
						ds.createQuery(DocumentDataImpl.class).field("_id").equal(docId),
						ds.createUpdateOperations(DocumentDataImpl.class)
								.set("attach", attIdnt)
								.set("diffs", doc.getDiffs()).set("desc", doc.getDescription())
								.set("updateDate", new Date())
								.set("profileAreas", doc.getProfile().getProfileAreas()))
						.getUpdatedExisting() &&
				// Update node name
				ds.update(
						ds.createQuery(NodeDataImpl.class).field("_id").equal(doc.getNode().getId()),
						ds.createUpdateOperations(NodeDataImpl.class)
						   .set("name", doc.getIdentity().getName()))
						   .getUpdatedExisting();
	}

	@Override
	public boolean tupDocument(String docId, String userId) {
		try {
			return ds.update(
					ds.createQuery(DocumentDataImpl.class).field("_id").equal(docId),
					ds.createUpdateOperations(DocumentDataImpl.class).add("tups",
							getAccountIdentity(userId), false)).getUpdatedExisting();
		} catch (PersistenceException e) {
			LOG.error("Account wtih given id " + userId + " not found");
		}

		return false;
	}

	/*
	 * Discussion store
	 */
	private Class getDiscussionClassByEnum(DiscussionSubject subject) {
		if (subject.equals(DiscussionSubject.DOCUMENT))
			return DocumentDataImpl.class;
		else
			return AttachmentInfoImpl.class;
	}

	@Override
	public Discussion getDiscussion(String id, DiscussionSubject subject, String... fields) {
		return morphia.fromDBObject(
				DiscussionImpl.class,
				(DBObject) ds
						.getCollection(getDiscussionClassByEnum(subject))
						.findOne(
								new BasicDBObject("_id", id),
								new BasicDBObject("discussion", 1)).get("discussion"));
	}

	@Override
	public Discussion getDiscussion(String id, DiscussionSubject subject, int offset, int limit) {
		BasicDBList list = new BasicDBList();
		list.put(0, (offset + limit) * (-1));
		list.put(1, limit);

		return morphia
				.fromDBObject(
						DiscussionImpl.class,
						(DBObject) ds
								.getCollection(getDiscussionClassByEnum(subject))
								.findOne(
										new BasicDBObject("_id", id),
										new BasicDBObject("discussion", 1).append("discussion.messages",
												new BasicDBObject("$slice", (offset == 0) ? limit * (-1)
														: list))).get("discussion"));
	}

	@Override
	public boolean addMessage(String id, DiscussionSubject subject, Message mess) {
		return ds
				.getCollection(getDiscussionClassByEnum(subject))
				.update(new BasicDBObject("_id", id),
						new BasicDBObject("$push", new BasicDBObject("discussion.messages", morphia
								.toDBObject(mess))).append("$inc", new BasicDBObject("discussion.count",
								1))).getN() > 0;
	}

	@Override
	public boolean removeMessage(String id, DiscussionSubject subject, String messId) {
		return ds
				.getCollection(getDiscussionClassByEnum(subject))
				.update(new BasicDBObject("_id", id).append("discussion.messages.id", messId),
						new BasicDBObject("$pull", new BasicDBObject("discussion.messages",
								new BasicDBObject("id", messId)))).getN() > 0;
	}

	@Override
	public boolean changeMessage(String id, DiscussionSubject subject, Message mess) {
		return ds
				.getCollection(getDiscussionClassByEnum(subject))
				.update(new BasicDBObject("_id", id).append("discussion.messages.id", mess.getId())
						.append("discussion.messages.author",
								new DBRef(ds.getDB(), ACCOUNT_IDENTITY_COLLECTION, mess.getAuthor()
										.getId())),
						new BasicDBObject("$set", new BasicDBObject("discussion.messages.$.text", mess
								.getText()))).getN() > 0;
	}

	/*
	 * Torrent store
	 */
	@Override
	public boolean createTorrent(TorrentData torrent) {
		if (ds.exists(torrent) == null) {
			ds.save(torrent);
			return true;
		}

		return false;
	}

	@Override
	public boolean updateTorrent(TorrentData torrent) {
		ds.save(torrent);
		return true;
	}

	@Override
	public boolean removeTorrent(String infoHash) {
		return ds.delete(TorrentDataImpl.class, infoHash).getN() > 0;
	}

	@Override
	public TorrentData getTorrent(String infoHash) throws PersistenceException {
		TorrentData torr = ds.get(TorrentDataImpl.class, infoHash);

		if (torr == null)
			throw new PersistenceException();

		return torr;
	}

	@Override
	public Iterable<TorrentData> getTorrents() {
		return (Iterable<TorrentData>) (Iterable<?>) ds.createQuery(TorrentDataImpl.class).fetch();
	}


}
